/*
JSCAD Object to OBJ Format Serialization

## License

Copyright (c) 2021 JSCAD Organization https://github.com/jscad

All code released under MIT license

Notes:
1) geom2 conversion to:
     none
2) geom3 conversion to:
     mesh
3) path2 conversion to:
     none

*/

/**
 * Serializer of JSCAD geometries to OBJ source data
 *
 * The serialization of the following geometries are possible.
 * - serialization of 3D geometry (geom3) to OBJ object (a unique mesh containing both vertices and volumes)
 *
 * @module io/obj-serializer
 * @example
 * const { serializer, mimeType } = require('@jscad/obj-serializer')
 */

const { colors, geometries, modifiers } = require('@jscad/modeling')

const { flatten, toArray } = require('@jscad/array-utils')

const mimeType = 'application/object'

/**
 * Serialize the give objects (geometry) to OBJ source data.
 * @param {Object} options - options for serialization
 * @param {Boolean} [options.triangulate=true] - triangle or polygon faces
 * @param {Function} [options.statusCallback] - call back function for progress ({ progress: 0-100 })
 * @param {...Object} objects - objects to serialize into OBJ source data
 * @returns {Array} serialized contents, OBJ source data
 * @alias module:io/obj-serializer.serialize
 * @example
 * const geometry = primitives.cube()
 * const objData = serializer({}, geometry)
 */
const serialize = (options, ...objects) => {
  const defaults = {
    statusCallback: null,
    triangulate: true // OBJ file supports polygon faces, but triangulate by default for safety
  }
  options = Object.assign({}, defaults, options)

  objects = flatten(objects)

  // convert only 3D geometries
  let objects3d = objects.filter((object) => geometries.geom3.isA(object))

  if (objects3d.length === 0) throw new Error('only 3D geometries can be serialized to OBJ')
  if (objects.length !== objects3d.length) console.warn('some objects could not be serialized to OBJ')

  // snap to grid and convert to triangles
  objects3d = toArray(modifiers.generalize({ snap: true, triangulate: options.triangulate }, objects3d))

  options.statusCallback && options.statusCallback({ progress: 0 })

  // construct the contents of the OBJ file
  let body = '# Wavefront OBJ file generated by JSCAD\n'

  // find unique vertices - use Map for O(1) lookup instead of O(n) indexOf
  const vertexMap = new Map() // vertex string -> index (1-based)

  // convert objects
  // TODO: group objects together
  let previousColor = 'default'
  objects3d.forEach((object, i) => {
    options.statusCallback && options.statusCallback({ progress: 100 * i / objects3d.length })
    body += '\n'

    const objectColor = getColorName(object)
    const polygons = geometries.geom3.toPolygons(object)
      .filter((p) => p.vertices.length >= 3)

    polygons.forEach((polygon) => {
      polygon.vertices.forEach((vertex) => {
        const vertexString = convertVertex(vertex)
        if (!vertexMap.has(vertexString)) {
          // add unique vertices
          const index = vertexMap.size + 1 // OBJ uses 1-based indexing
          vertexMap.set(vertexString, index)
          body += `${vertexString}\n`
        }
      })
    })
    body += '\n'

    // convert faces
    polygons.forEach((polygon) => {
      // convert vertices to indices
      const indices = polygon.vertices
        .map((v) => vertexMap.get(convertVertex(v)))
      // set face color
      const color = getColorName(polygon) || objectColor || 'default'
      if (color !== previousColor) {
        body += `usemtl ${color}\n`
        previousColor = color
      }
      body += `f ${indices.join(' ')}\n`
    })
  })

  options.statusCallback && options.statusCallback({ progress: 100 })

  return [body]
}

/**
 * Convert a vertex to an obj "v" string
 */
const convertVertex = (vertex) => `v ${vertex[0]} ${vertex[1]} ${vertex[2]}`

/**
 * Get the closest css color name
 */
const getColorName = (object) => {
  let colorName
  if (object.color) {
    const r = object.color[0]
    const g = object.color[1]
    const b = object.color[2]
    // find the closest css color
    let closest = 255 + 255 + 255
    for (const name in colors.cssColors) {
      const rgb = colors.cssColors[name]
      const diff = Math.abs(r - rgb[0]) + Math.abs(g - rgb[1]) + Math.abs(b - rgb[2])
      if (diff < closest) {
        colorName = name
        if (diff === 0) break
        closest = diff
      }
    }
  }
  return colorName
}

module.exports = {
  serialize,
  mimeType
}
